package cyce.server;

import java.io.BufferedWriter;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.FileReader;
import java.io.IOException;
import java.rmi.ConnectException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import cyce.common.ConfigParser;
import cyce.file.Directory;
import cyce.file.File;
import cyce.file.FileFactory;
import cyce.peer.IPeer;
import cyce.peer.IServerPeer;

/**
 * This file implements the control server functionality
 * @author CYCE
 */
public class Server extends UnicastRemoteObject implements IPeerServer, IServer {

	public static enum MODE { Primary, Secondary };
	
	private static final long serialVersionUID = -9086951907977906412L;
	
	// format of the time we want to display	
	private static final String DATE_FORMAT = "H:mm:ss";

	private MODE m_mode;
	private boolean m_peerServerAlive;
	private int m_peerServerPort;
	private String m_peerServerName;
	
	private String xmlRootDumpFile;

	private IServer m_peerServer;

	Directory root;
	PeerContentMap pcMap = new PeerContentMap();
	Registry reg;
	
	public Server(String xmlPath, int registryPort, String peerServerName, int peerServerPort, MODE mode) throws RemoteException{
		 super();
		 reg = LocateRegistry.createRegistry(registryPort);
		 reg.rebind("server", this);
		 m_peerServerAlive = true;
		 m_peerServerPort = peerServerPort;
		 m_peerServerName = peerServerName;
		 m_mode = mode;
		 this.xmlRootDumpFile = xmlPath;
		 
		 this.readXML();
		 
		 //request content from other server, if it's up
		 getPeerServerStatus();
	}
	
	/* (non-Javadoc)
	 * @see cyce.server.IPeerServer#addContent(cyce.peer.IServerPeer, cyce.file.File)
	 */
	public synchronized void addContent(IServerPeer peer, File file) throws NotPrimaryServerException {
		//if we're the secondary server, check for the primary before proceeding
		if (this.m_mode == MODE.Secondary){
			checkForPrimary();
		}
		
		// add content to list of files in system and do all error checking
		FileFactory.addFile(root, file.getParentPath(), file.getName(), file.getHash());
		
		//if primary, syncronize state before proceeding
		if (this.m_mode == MODE.Primary){
			doPrimarySynchronization();
		} 
		
		// record that the connecting peer has the content
		pcMap.addPair(peer, file);
		
		// send content list updates to all peers
		UpdateThread ut = new UpdateThread();
	    ut.start();
	}

	/* (non-Javadoc)
	 * @see cyce.server.IPeerServer#deleteContent(cyce.file.File)
	 */
	public synchronized void deleteContent(File file) throws NotPrimaryServerException {
		// receives information about the file to be removed
			// the file hash
		
//		if we're the secondary server, check for the primary before proceeding
		if (this.m_mode == MODE.Secondary){
			checkForPrimary();
		}
		
		// remove content from list of files in the system and do all error checking
		FileFactory.removeFile(root, file.getPath());
		
//		if primary, syncronize state before proceeding
		if (this.m_mode == MODE.Primary){
			doPrimarySynchronization();
		} 
		
		// remove information about which peers have the file
		pcMap.removeFile(file);
		
		// send content list updates to all peers
		UpdateThread ut = new UpdateThread();
	    ut.start();

	}

	/* (non-Javadoc)
	 * @see cyce.server.IPeerServer#addDirectory(cyce.peer.IServerPeer, cyce.file.Directory)
	 */
	public synchronized void addDirectory(IServerPeer peer, Directory dir) throws RemoteException, NotPrimaryServerException {
//		if we're the secondary server, check for the primary before proceeding
		if (this.m_mode == MODE.Secondary){
			checkForPrimary();
		}
		
		// add directory to the system and do all error checking
		FileFactory.addDirectory(root, dir.getParentPath(), dir.getName());
	
//		if primary, syncronize state before proceeding
		if (this.m_mode == MODE.Primary){
			doPrimarySynchronization();
		} 
		
		// send content list updates to all peers
		UpdateThread ut = new UpdateThread();
	    ut.start();
	}
	
	/* (non-Javadoc)
	 * @see cyce.server.IPeerServer#deleteDirectory(cyce.file.Directory)
	 */
	public synchronized void deleteDirectory(Directory dir) throws RemoteException, NotPrimaryServerException {
//		if we're the secondary server, check for the primary before proceeding
		if (this.m_mode == MODE.Secondary){
			checkForPrimary();
		}		
		
		// remove directory from the system and do all error checking
		FileFactory.removeFile(root, dir.getPath());
	
//		if primary, syncronize state before proceeding
		if (this.m_mode == MODE.Primary){
			doPrimarySynchronization();
		} 
		
		// send content list updates to all peers
		UpdateThread ut = new UpdateThread();
	    ut.start();
	}
	
	/* (non-Javadoc)
	 * @see cyce.server.IPeerServer#deregister(cyce.peer.IServerPeer)
	 */
	public void deregister(IServerPeer peer) {
		// receives information about the peer to be removed
		// from the network
		
		// removes information about files that the peers has
		pcMap.removePeer(peer);
	}

	/* (non-Javadoc)
	 * @see cyce.server.IPeerServer#register(cyce.peer.IServerPeer, java.util.List)
	 */
	public Directory register(IServerPeer peer, List<File> files) throws NotPrimaryServerException {
//		if we're the secondary server, check for the primary before proceeding
		if (this.m_mode == MODE.Secondary){
			checkForPrimary();
		}
		
		// receives information about the peer to be added to the network
			// includes a list of all content that the peer has
		
		List<File> filesToRemove = new ArrayList<File>();
		
		// error checks
		// ensure that content on the peer is still valid (not deleted)
		for (File file : files){
			if (!FileFactory.fileExists(root, file)){
				filesToRemove.add(file);
			}
		}
		files.removeAll(filesToRemove);
			
		// add peer and content to server list
		pcMap.addPeer(peer, files);
		
		// return the updated file list
		return root;

	}

	/* (non-Javadoc)
	 * @see cyce.server.IPeerServer#requestContent(cyce.file.File)
	 */
	public List<IPeer> requestContent(File file) throws NotPrimaryServerException {
		// receives information about the content being requested
			// the hash
		
//		if we're the secondary server, check for the primary before proceeding
		if (this.m_mode == MODE.Secondary){
			checkForPrimary();
		}
		
		// returns a list of peers that have the content
		List<IPeer> retval = pcMap.getPeerList(file);
		if (retval == null) retval = new LinkedList<IPeer>();
		return retval;
	}

	/* (non-Javadoc)
	 * @see cyce.server.IPeerServer#advertiseFile(cyce.peer.IServerPeer, cyce.file.File)
	 */
	public void advertiseFile(IServerPeer peer, File file) throws NotPrimaryServerException {
//		if we're the secondary server, check for the primary before proceeding
		if (this.m_mode == MODE.Secondary){
			checkForPrimary();
		}
		
		//error check that the file is in directory structure
		
		pcMap.addPair(peer, file);
		
	}

	/* (non-Javadoc)
	 * @see cyce.server.IPeerServer#unadvertiseFile(cyce.peer.IServerPeer, cyce.file.File)
	 */
	public void unadvertiseFile(IServerPeer peer, File file) {
		pcMap.removePair(peer, file);	
	}

	/* (non-Javadoc)
	 * @see cyce.server.IServer#areYouAlive()
	 */
	public boolean areYouAlive()
	{
		return true;
	}

	/* (non-Javadoc)
	 * @see cyce.server.IServer#notifyChange(cyce.file.Directory)
	 */
	public void notifyChange(Directory root)
	{
		this.root = root;
		System.out.println(getCurrTime() + ": got root");
	}

	/* (non-Javadoc)
	 * @see cyce.server.IServer#getState()
	 */
	public synchronized Directory getState()
	{
		if (this.m_mode == MODE.Secondary){
			//the primary server is back
			//give the primary server our state then asychronously tell all 
			//peers to reconnect to primary
			DisconnectThread dt = new DisconnectThread();
		    dt.start();
		}
		return root;
	}

	/**
	 * Retrieve a string representation of the current time
	 * @return
	 */
	private String getCurrTime()
	{
		Date today = new Date();
		SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
		return formatter.format(today);
	}

	/**
	 * Read the persisted file store information in the XML
	 */
	public void readXML() {
		if (this.xmlRootDumpFile != null){
			String xml = "";
			try {
				BufferedReader reader = new BufferedReader(new FileReader(this.xmlRootDumpFile));
				String line = reader.readLine();
				while (line != null) {
					xml += line;
					line = reader.readLine();
				}
				reader.close();
			}
			catch (FileNotFoundException fnfe) {
				fnfe.printStackTrace();
				return;
			}
			catch (IOException ioe) {
				ioe.printStackTrace();
				return;
			}
			
			this.root = FileFactory.deserializeDirectory(xml);
		}
	}
	
	/**
	 * Persist state information to the XML
	 */
	public void writeXML() {
		if (this.xmlRootDumpFile != null){
			String xml = FileFactory.serialize(root);
			try {
				BufferedWriter writer = new BufferedWriter(new FileWriter(this.xmlRootDumpFile));
				writer.write(xml);
				writer.close();
			}
			catch (FileNotFoundException fnfe) {
				fnfe.printStackTrace();
				return;
			}
			catch (IOException ioe) {
				ioe.printStackTrace();
				return;
			}
		}
	}
	
	/**
	 * Retrieve the state of the other server.
	 * Called when a server is restarted after a failure (or 
	 * started for the first time).
	 */
	private void getPeerServerStatus(){
		try {
			
//			get the registry on the primary server
			Registry registry = LocateRegistry.getRegistry(this.m_peerServerName,this.m_peerServerPort);
//			 attempt to connect to the other server
			m_peerServer = (IServer) registry.lookup("server");
	
			//check to see if the other server is alive
			m_peerServerAlive = m_peerServer.areYouAlive();
			
			//retrieve state and update
			if (m_peerServerAlive){
				this.root = m_peerServer.getState();
				//
			}
		}
		catch (NotBoundException nbe)
		{
			System.err.println("nbe: " + nbe.getMessage());
			return;
		}
		catch (RemoteException e) {
			System.err.println(getCurrTime() + " Peer server has crashed.");
			return;
		}
	}
	
	/**
	 * Check to see if the primary server is available.
	 * Called by the secondary server to force peers to always use the primary if
	 * it's available
	 * @throws NotPrimaryServerException
	 */
	private void checkForPrimary() throws NotPrimaryServerException{
		try {
		
//			get the registry on the primary server
			Registry registry = LocateRegistry.getRegistry(this.m_peerServerName,this.m_peerServerPort);
//			 attempt to connect to the other server
			m_peerServer = (IServer) registry.lookup("server");
	
			//check to see if the other server is alive
			m_peerServerAlive = m_peerServer.areYouAlive();
			
			System.out.println(getCurrTime() + " Primary server alive, redirecting peers");
			throw new NotPrimaryServerException();
		}
		catch (NotBoundException nbe)
		{
			System.err.println("nbe: " + nbe.getMessage());
			return;
		}
		catch (RemoteException e) {
			System.err.println(getCurrTime() + " Peer server has crashed.");
			return;
		}
	}
	
	/**
	 * Force a state change on the secondary server.
	 * Called only by the primary server.
	 */
	private void doPrimarySynchronization(){
		try
		{
//			get the registry on the primary server
			Registry registry = LocateRegistry.getRegistry(this.m_peerServerName,this.m_peerServerPort);
//			 attempt to connect to the other server
			m_peerServer = (IServer) registry.lookup("server");

			//check to see if the other server is alive
			m_peerServerAlive = m_peerServer.areYouAlive();
			
			System.out.println(getCurrTime() + " My server peer is alive!");

			if (m_peerServerAlive){
//				primary server forces information on secondary server 
				m_peerServer.notifyChange(root);	
			}
		}
		catch (NotBoundException nbe)
		{
			System.err.println("nbe: " + nbe.getMessage());
			return;
		}
		catch (RemoteException e) {
			System.err.println(getCurrTime() + " Peer server has crashed.");
			return;
		}
	}
	
	
	/**
	 * Used to asyncronously inform all peers to attempt to reconnect to the server
	 */
	class DisconnectThread extends Thread {
        
        public void run() {
//        	 send content list updates to all peers
    		List<IServerPeer> peers = pcMap.getPeers();
    		List<IServerPeer> listCopy = new LinkedList<IServerPeer>();
    		listCopy.addAll(peers);
    		for (IServerPeer p : listCopy){
    			try
    			{
    				p.forceReconnect();
    			}
    			catch (ConnectException e){
    				//peer has been disconnected
    				deregister(p);
    			}
    			catch (RemoteException e)
    			{
    				//TODO
    				e.printStackTrace();
    			}
    		}
        }
    }
	
	/**
	 * Used to asyncronously inform all peers of a new network state
	 */
	class UpdateThread extends Thread {
        
        public void run() {
        	writeXML();
        	
//        	 send content list updates to all peers
    		List<IServerPeer> peers = pcMap.getPeers();
    		List<IServerPeer> listCopy = new LinkedList<IServerPeer>();
    		listCopy.addAll(peers);
    		for (IServerPeer p : listCopy){
    			try
    			{
    				p.updateFileList(root);
    			}
    			catch (ConnectException e){
    				//peer has been disconnected
    				deregister(p);
    				System.err.println("Removed unreachable peer from list.");
    			}
    			catch (RemoteException e)
    			{
    				//TODO
    				e.printStackTrace();
    			}
    		}
        }
    }


	public static void main(String [] args)
	{
		try {
			
			ConfigParser config = new ConfigParser("serverConfig.xml");
			
			//TODO: Parse the file XML
			
			int registryPort = 0;
			int connectPort = 0;

			if (args.length != 1)
			{
				System.out.println("Usage [primary|secondary]");
				System.exit(-1);
			}

			String mode = args[0];
			MODE serverMode;
			String peerServerName;
			
			if (mode.equals("primary"))
			{
				registryPort = Integer.parseInt(config.getValue("primaryServerPort"));
				connectPort = Integer.parseInt(config.getValue("secondaryServerPort"));
				peerServerName = config.getValue("secondaryServerName");
				serverMode = MODE.Primary;
			}
			else
			{
				registryPort = Integer.parseInt(config.getValue("secondaryServerPort"));
				connectPort = Integer.parseInt(config.getValue("primaryServerPort"));
				serverMode = MODE.Secondary;
				peerServerName = config.getValue("primaryServerName");
			}
				
			String xmlPath = config.getValue("fileXMLPath");

			Server server = new Server(xmlPath, registryPort, peerServerName, connectPort, serverMode);
		
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	
}
